<FrameworkSwitchCourse {fw} />

# Token 分类 [[Token 分类]]

{#if fw === 'pt'}

<DocNotebookDropdown
  classNames="absolute z-10 right-0 top-0"
  options={[
    {label: "Google Colab", value: "https://colab.research.google.com/github/huggingface/notebooks/blob/master/course/chapter7/section2_pt.ipynb"},
    {label: "Aws Studio", value: "https://studiolab.sagemaker.aws/import/github/huggingface/notebooks/blob/master/course/chapter7/section2_pt.ipynb"},
]} />

{:else}

<DocNotebookDropdown
  classNames="absolute z-10 right-0 top-0"
  options={[
    {label: "Google Colab", value: "https://colab.research.google.com/github/huggingface/notebooks/blob/master/course/chapter7/section2_tf.ipynb"},
    {label: "Aws Studio", value: "https://studiolab.sagemaker.aws/import/github/huggingface/notebooks/blob/master/course/chapter7/section2_tf.ipynb"},
]} />

{/if}

我们将首先探讨的应用是 Token 分类。这个通用任务涵盖了所有可以表述为“给句子中的词或字贴上标签”的问题，例如：

- **实体命名识别 （NER）**：找出句子中的实体（如人物、地点或组织）。这可以通过为每个实体指定一个类别的标签，如果没有实体则会输出无实体的标签。
- **词性标注 （POS）**：将句子中的每个单词标记为对应于特定的词性（如名词、动词、形容词等）。
- **分块（chunking）**：找出属于同一实体的 tokens 这个任务（可以与词性标注或命名实体识别结合）可以被描述为将位于块开头的 token 赋予一个标签（通常是 “ `B-` ” （Begin），代表该token位于实体的开头），将位于块内的 tokens 赋予另一个标签（通常是 “ `I-` ”（inner）代表该token位于实体的内部），将不属于任何块的 tokens 赋予第三个标签（通常是 “ `O` ” （outer）代表该token不属于任何实体）。
<Youtube id="wVHdVlPScxA"/>

当然，还有很多其他类型的 token 分类问题；这些只是几个有代表性的例子。在本节中，我们将在 NER 任务上微调模型 （BERT），然后该模型将能够生成如下预测：

<iframe src="https://course-demos-bert-finetuned-ner.hf.space" frameBorder="0" height="350" title="Gradio app" class="block dark:hidden container p-0 flex-grow space-iframe" allow="accelerometer; ambient-light-sensor; autoplay; battery; camera; document-domain; encrypted-media; fullscreen; geolocation; gyroscope; layout-animations; legacy-image-formats; magnetometer; microphone; midi; oversized-images; payment; picture-in-picture; publickey-credentials-get; sync-xhr; usb; vr ; wake-lock; xr-spatial-tracking" sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-scripts allow-downloads"></iframe>

<a class="flex justify-center" href="/huggingface-course/bert-finetuned-ner">
<img class="block dark:hidden lg:w-3/5" src="https://huggingface.co/datasets/huggingface-course/documentation-images/resolve/main/en/chapter7/model-eval-bert-finetuned-ner.png" alt="One-hot encoded labels for question answering."/>
<img class="hidden dark:block lg:w-3/5" src="https://huggingface.co/datasets/huggingface-course/documentation-images/resolve/main/en/chapter7/model-eval-bert-finetuned-ner-dark.png" alt="One-hot encoded labels for question answering."/>
</a>

你可以 [在这里](https://huggingface.co/huggingface-course/bert-finetuned-ner?text=My+name+is+Sylvain+and+I+work+at+Hugging+Face+in+Brooklyn) 。找到我们将训练并上传到 Hub 的模型，可以尝试输入一些句子测试一下模型的预测结果。

## 准备数据 [[准备数据]]

首先，我们需要一个适合 token 分类的数据集。在本节中，我们将使用 [CoNLL-2003 数据集](https://huggingface.co/datasets/conll2003) ，该数据集来源于路透社的新闻报道。

<Tip>

💡 只要你的数据集由分词文本和对应的标签组成，就能够将这里描述的数据处理过程应用到自己的数据集中。如果需要复习如何在 `Dataset` 中加载自定义数据集，请复习 [第五章](/course/chapter5) 。

</Tip>

### CoNLL-2003 数据集 [[CoNLL-2003 数据集]]

要加载 CoNLL-2003 数据集，我们可以使用🤗 Datasets 库的 `load_dataset()` 方法：

```py
from datasets import load_dataset

raw_datasets = load_dataset("conll2003")
```

这将下载并缓存数据集，就像和我们在 [第三章](/course/chapter3) 加载 GLUE MRPC 数据集一样。查看 `raw_datasets` 对象可以让我们看到数据集中存在哪些列，以及训练集、验证集和测试集之间是如何划分的：

```py
raw_datasets
```

```python out
DatasetDict({
    train: Dataset({
        features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
        num_rows: 14041
    })
    validation: Dataset({
        features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
        num_rows: 3250
    })
    test: Dataset({
        features: ['chunk_tags', 'id', 'ner_tags', 'pos_tags', 'tokens'],
        num_rows: 3453
    })
})
```

可以看到数据集包含了我们之前提到的三项任务的标签：命名实体识别（NER）、词性标注（POS）以及分块（chunking）。这个数据集与其他数据集的一个显著区别在于，输入文本的那一列并非以句子或整片的文本的形式存储，而是以单词列表的形式（最后一列被称为 `tokens` ，不过 `tokens` 列保存的还是单词，也就是说，这些预先分词的输入仍需要经过 tokenizer 进行子词分词处理）。

我们来看看训练集的第一个元素：

```py
raw_datasets["train"][0]["tokens"]
```

```python out
['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.']
```

由于我们要进行命名实体识别，让我们查看一下 NER 标签：

```py
raw_datasets["train"][0]["ner_tags"]
```

```python out
[3, 0, 7, 0, 0, 0, 7, 0, 0]
```

这些是可以直接训练的整数类别标签，当我们想要检查数据时，直接看整数的标签不是很直观。就像在文本分类中，我们可以通过查看数据集的 `features` 属性来找到这些整数和标签名称之间的对应关系：

```py
ner_feature = raw_datasets["train"].features["ner_tags"]
ner_feature
```

```python out
Sequence(feature=ClassLabel(num_classes=9, names=['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC'], names_file=None, id=None), length=-1, id=None)
```

因此，我们得到了 `ClassLabels` 类型的序列。序列中元素的类型存储在 `ner_feature` 的 `feature` 中，我们可以通过查看该 `feature` 的 `names` 属性来访问标签名称的列表：

```py
label_names = ner_feature.feature.names
label_names
```

```python out
['O', 'B-PER', 'I-PER', 'B-ORG', 'I-ORG', 'B-LOC', 'I-LOC', 'B-MISC', 'I-MISC']
```

我们在 [第六章](/course/chapter6/3) 深入研究 `token-classification` 管道时已经看到过这些标签 我们在这里进行一个快速的回顾：

- `O` 表示这个词不对应任何实体。
- `B-PER` / `I-PER` 意味着这个词对应于人名实体的开头/内部。
- `B-ORG` / `I-ORG` 的意思是这个词对应于组织名称实体的开头/内部。
- `B-LOC` / `I-LOC` 指的是是这个词对应于地名实体的开头/内部。
- `B-MISC` / `I-MISC` 表示这个词对应一个其他实体（不属于特定类别或类别之外）的开头 / 内部。

接下来使用对这些标签名对数据集的 `ner_tags` 进行解码，将得到以下输出结果:

```python
words = raw_datasets["train"][0]["tokens"]
labels = raw_datasets["train"][0]["ner_tags"]
line1 = ""
line2 = ""
for word, label in zip(words, labels):
    full_label = label_names[label]
    max_length = max(len(word), len(full_label))
    line1 += word + " " * (max_length - len(word) + 1)
    line2 += full_label + " " * (max_length - len(full_label) + 1)

print(line1)
print(line2)
```

```python out
'EU    rejects German call to boycott British lamb .'
'B-ORG O       B-MISC O    O  O       B-MISC  O    O'
```

我们还可以查看训练集中索引为 4 的数据，它是一个同时包含 `B-` 和 `I-` 标签的例子：

```python out
'Germany \'s representative to the European Union \'s veterinary committee Werner Zwingmann said on Wednesday consumers should buy sheepmeat from countries other than Britain until the scientific advice was clearer .'
'B-LOC   O  O              O  O   B-ORG    I-ORG O  O          O         B-PER  I-PER     O    O  O         O         O      O   O         O    O         O     O    B-LOC   O     O   O          O      O   O       O'
```

正如我们在上面的输出中所看到的，跨越两个单词的实体，如“European Union”和“Werner Zwingmann”，数据集把第一个单词标注为了 `B-` 标签，将第二个单词标记为了 `I-` 标签。

<Tip>

✏️ **轮到你了！** 检查同一个句子的词性标注 （POS）或分块（chunking）列，查看输出的结果。

</Tip>

### 处理数据 [[处理数据]]

<Youtube id="iY2AZYdZAr0"/>

像往常一样，我们的文本需要转换为 Token ID，然后模型才能理解它们。正如我们在 [第六章](/course/chapter6/) 所学的那样。不过与 tokens 分类任务不同的是数据集已经完成了预分词，我们在处理的过程中不需要再次分词。幸运的是，tokenizer API 可以很容易地处理这种差异；我们只需要通过一个特殊的标志告诉 tokenizer即可。

首先，让我们创建 `tokenizer` 对象。如前所述，我们将使用 BERT 预训练模型，因此我们将从下载并缓存关联的 `tokenizer` 开始：

```python
from transformers import AutoTokenizer

model_checkpoint = "bert-base-cased"
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
```

你可以更换把 `model_checkpoint` 更换为 [Hub](https://huggingface.co/models) 上你喜欢的任何其他模型，或使用本地保存的预训练模型和 `tokenizer`。唯一的限制是 tokenizer 需要由 🤗 Tokenizers 库支持，并且有一个“快速”版本可用。你可以在 [Huggingface 模型后端支持表](https://huggingface.co/transformers/#supported-frameworks) 上看到所有带有快速版本的 `tokenizer` 架构，或者你也可以通过查看它 `is_fast` 属性来检测正在使用的 `tokenizer` 对象是否由 🤗 Tokenizers 支持：

```py
tokenizer.is_fast
```

```python out
True
```

我们可以像往常一样使用我们的 `tokenizer` 对预先分词的输入进行 `tokenize` ，只需额外添加 `is_split_into_words=True` 参数：

```py
inputs = tokenizer(raw_datasets["train"][0]["tokens"], is_split_into_words=True)
inputs.tokens()
```

```python out
['[CLS]', 'EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'la', '##mb', '.', '[SEP]']
```

如我们所见，`tokenizer` 在结果中添加了模型需要使用的特殊 tokens（在开头的 `[CLS]` ，在结尾的 `[SEP]` ），并且大部分单词保持不变。不过，单词 `lamb` 被分词为两个子词， `la` 和 `##mb` 。这导致了输入和标签之间的不匹配：标签列表只有 9 个元素，而我们的输入现在有 12 个 tokens。解决特殊 tokens 的问题很容易（我们已经知道了每个 `token` 开始和结束的位置），我们需要确保的是我们将所有的标签与正确的词对齐。


幸运的是，由于我们使用的是快速 tokens 因此我们可以使用🤗 Tokenizers 超能力，这意味着我们可以轻松地将每个 token 映射到其相应的单词（如 [第六章](/course/chapter6/3) 中所学）：

```py
inputs.word_ids()
```

```python out
[None, 0, 1, 2, 3, 4, 5, 6, 7, 7, 8, None]
```

只需要一点点工作，我们就可以扩展我们的标签列表。我们将添加的使其与 `token` 列表相匹配。 添加的第一条规则是，特殊 tokens 的标签设置为 `-100` 。这是因为默认情况下， `-100` 会被我们的损失函数（交叉熵）忽略。然后将每个 `token` 的标签设置为这个 `token` 所在单词开头的 `token` 的标签，因为同一个单词一定是同一个实体的一部分。最后将单词的内部 `tokens` 标签中的 `B-` 替换为 `I-`，因为该 `token` 不在实体的开头，`B-` 在每个实体中只应该出现一次：

```python
def align_labels_with_tokens(labels, word_ids):
    new_labels = []
    current_word = None
    for word_id in word_ids:
        if word_id != current_word:
            # 新单词的开始!
            current_word = word_id
            label = -100 if word_id is None else labels[word_id]
            new_labels.append(label)
        elif word_id is None:
            # 特殊的token
            new_labels.append(-100)
        else:
            # 与前一个 tokens 类型相同的单词
            label = labels[word_id]
            # 如果标签是 B-XXX 我们将其更改为 I-XXX
            if label % 2 == 1:
                label += 1
            new_labels.append(label)

    return new_labels
```

让我们在数据集的第一个元素上试一试：

```py
labels = raw_datasets["train"][0]["ner_tags"]
word_ids = inputs.word_ids()
print(labels)
print(align_labels_with_tokens(labels, word_ids))
```

```python out
[3, 0, 7, 0, 0, 0, 7, 0, 0]
[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100]
```

正如我们所看到的，我们的函数为开头和结尾添加了两个特殊 tokens ：`-100` ，并为切分成两个 tokens 的单词添加了一个新的 `0` 标签。

<Tip>

✏️ **轮到你了！** 有些研究人员更喜欢只为每个单只分配一个标签，对该单词其他部分的 token 分配 `-100` 标签。这是为了避免那些分解成许多子词的长单词对损失作出过多的贡献。请按照这个思路，改变之前的函数，使标签与 inputs ID 对齐。

</Tip>

为了预处理我们的整个数据集，我们需要对所有输入进行 `tokenize`，并使用 `align_labels_with_tokens()` 函数处理所有标签。为了充分利用快速 tokenizer 的优势，最好是同时对大量文本一起进行 `tokenize`，所以我们需要编写一个处理一组示例的函数，并使用带有 `batched=True` 参数的 `Dataset.map()` 方法。与我们之前的示例唯一不同的是，当 `tokenizer` 的输入是文本列表（或示例中单词的列表的列表）时， `word_ids()` 函数需要根据列表的索引获取 `token` 的 ID，所以我们也在下面的函数中添加了这个功能：

```py
def tokenize_and_align_labels(examples):
    tokenized_inputs = tokenizer(
        examples["tokens"], truncation=True, is_split_into_words=True
    )
    all_labels = examples["ner_tags"]
    new_labels = []
    for i, labels in enumerate(all_labels):
        word_ids = tokenized_inputs.word_ids(i)
        new_labels.append(align_labels_with_tokens(labels, word_ids))

    tokenized_inputs["labels"] = new_labels
    return tokenized_inputs
```

注意，我们还没有填充我们的输入，我们将在稍后使用数据整理器创建 `batch` 时进行。

我们现在可以一次性使用预处理函数处理整个数据集：

```py
tokenized_datasets = raw_datasets.map(
    tokenize_and_align_labels,
    batched=True,
    remove_columns=raw_datasets["train"].column_names,
)
```

我们已经完成了最困难的部分！现在数据已经经过了预处理，实际的训练过程将会与我们在 [第三章](/course/chapter3) 所做的很相似。

{#if fw === 'pt'}

## 使用 Trainer API 微调模型 [[使用 Trainer API 微调模型]]

使用 `Trainer` 的代码会和以前一样，唯一的变化是数据如何整理成 `batch` 以及计算评估的分数。

{:else}

## 使用 Keras 微调模型 [[使用 Keras 微调模型]]

使用 `Keras` 的代码将与之前非常相似；唯一的变化是数据如何整理成 `batch` 以及计算评估的分数。

{/if}

### 整理数据 [[整理数据]]

我们不能像 [第三章](/course/chapter3) 那样直接使用 `DataCollatorWithPadding` ，因为那样只会填充输入（inputs ID、注意掩码和 tokens 类型 ID）。除了输入部分，在这里我们还需要对标签也使用与输入完全相同的方式填充，以保证它与输入的大小相同。我们将使用 `-100` 进行填充，以便在损失计算中忽略填充的内容。

以上的这些过程可以由 [`DataCollatorForTokenClassification`](https://huggingface.co/transformers/main_classes/data_collator.html#datacollatorfortokenclassification) 实现。它是一个带有填充功能的数据整理器，使用时只需要传入用于预处理输入的 `tokenizer` ：

{#if fw === 'pt'}

```py
from transformers import DataCollatorForTokenClassification

data_collator = DataCollatorForTokenClassification(tokenizer=tokenizer)
```

{:else}

```py
from transformers import DataCollatorForTokenClassification

data_collator = DataCollatorForTokenClassification(
    tokenizer=tokenizer, return_tensors="tf"
)
```

{/if}

为了在几个样本上测试这个数据整理器，我们可以先在训练集中的几个示例上调用它：

```py
batch = data_collator([tokenized_datasets["train"][i] for i in range(2)])
batch["labels"]
```

```python out
tensor([[-100,    3,    0,    7,    0,    0,    0,    7,    0,    0,    0, -100],
        [-100,    1,    2, -100, -100, -100, -100, -100, -100, -100, -100, -100]])
```

将数据整理器的结果与数据集中未经处理的第一个和第二个元素的标签进行比较。

```py
for i in range(2):
    print(tokenized_datasets["train"][i]["labels"])
```

```python out
[-100, 3, 0, 7, 0, 0, 0, 7, 0, 0, 0, -100]
[-100, 1, 2, -100]
```

{#if fw === 'pt'}

正如我们所看到的，已经使用 `-100` 将第二组标签填充到了与第一组标签相同的长度。

{:else}

我们的数据整理器已准备就绪！现在让我们使用 `to_tf_dataset()` 方法创建一个 `tf.data.Dataset` 。除此之外，你还可以使用 `model.prepare_tf_dataset()` 来使用更少的模板代码来创建 `Dataset`——你将在本章的其他小节中看到这样的操作。
```py
tf_train_dataset = tokenized_datasets["train"].to_tf_dataset(
    columns=["attention_mask", "input_ids", "labels", "token_type_ids"],
    collate_fn=data_collator,
    shuffle=True,
    batch_size=16,
)

tf_eval_dataset = tokenized_datasets["validation"].to_tf_dataset(
    columns=["attention_mask", "input_ids", "labels", "token_type_ids"],
    collate_fn=data_collator,
    shuffle=False,
    batch_size=16,
)
```

下一站：模型本身。

{/if}

{#if fw === 'tf'}

### 初始化模型

由于我们正在研究 Token 分类问题，因此我们将使用 `TFAutoModelForTokenClassification` 类。实例化此模型时要记得传递我们标签的数量，最简单方法是将该数字传递给 `num_labels` 参数，但是如果我们想要创建一个就像我们在本节开头看到的那样的推理小部件，那么就需要用最标准的方法正确设置标签的对应关系。

最标准的方法是用两个字典 `id2label` 和 `label2id` 来设置它们，这两个字典包含从 ID 到标签的映射以及反向的映射：

```py
id2label = {str(i): label for i, label in enumerate(label_names)}
label2id = {v: k for k, v in id2label.items()}
```

现在我们只需将这两个字典传递给 `TFAutoModelForTokenClassification.from_pretrained()` 方法，它们就会被保存在模型的配置中，然后被正确地保存和上传到 Hub：

```py
from transformers import TFAutoModelForTokenClassification

model = TFAutoModelForTokenClassification.from_pretrained(
    model_checkpoint,
    id2label=id2label,
    label2id=label2id,
)
```

就像我们在 [第三章](/course/chapter3) 中实例化 `TFAutoModelForTokenClassification` 类一样 创建模型会发出一个警告，提示一些权重未被使用（来自预训练模型头部的权重）和一些其他权重被随机初始化（来自新 Token 分类头部的权重），别担心，我们马上就要训练这些权重。首先让我们确认一下我们的模型是否具有正确的标签数量：

```python
model.config.num_labels
```

```python out
9
```

<Tip warning={true}>

⚠️ 如果你的模型的标签数量错误，那么在后面调用 `model.fit()` 时，你会得到一个晦涩的错误。这可能会令人烦恼，所以确保你做了这个检查，确认你的标签数量是正确的。

</Tip>

### 微调模型

现在，我们已准备好训练模型了！不过，我们在这之前还要做两件事：登录到 Hugging Face 并设置我们的训练超参数。如果你在 notebook 上工作，有一个便捷函数可以帮助你做到这一点：

```python
from huggingface_hub import notebook_login

notebook_login()
```

这将显示一个小部件，你可以在其中输入你的 Hugging Face 账号和密码。

如果你不是在 notebook 上工作，只需在终端中输入以下代码：

```bash
huggingface-cli login
```

登录后，我们可以编译我们模型所需要的所有配置。🤗 Transformers 提供了一个便捷的 `create_optimizer()` 函数，与 `TensorFlow` 内置的 `Adam` 优化器相比，它可以提供一个带有恰当的权重衰减和学习率衰减机制的 `AdamW` 优化器，不过这两个机制都将提高模型的性能：

```python
from transformers import create_optimizer
import tensorflow as tf

# 在混合精度 float16 中进行训练
tf.keras.mixed_precision.set_global_policy("mixed_float16")

# 训练步数是数据集中的样本数量,除以 batch 大小,然后乘以总的 epoch 数。
# 注意这里的 tf_train_dataset 是 batch 形式的 tf.data.Dataset,
# 而不是原始的 Hugging Face Dataset ,所以使用 len() 计算它的长度已经是 num_samples // batch_size。
num_epochs = 3
num_train_steps = len(tf_train_dataset) * num_epochs

optimizer, schedule = create_optimizer(
    init_lr=2e-5,
    num_warmup_steps=0,
    num_train_steps=num_train_steps,
    weight_decay_rate=0.01,
)
model.compile(optimizer=optimizer)
```

请注意，我们没有给 `compile()` 提供 `loss` 参数。这是因为模型实际上可以使用默认的方式自动计算损失 —— 如果你在编译时没有提供损失的计算方法或在输入中提供真实的标签值（就像我们在数据集中所做的那样），那么模型将使用内部默认的 `loss` 计算方法进行训练，具体的计算方式取决于你选择的任务和模型类型。

接下来，我们定义一个 `PushToHubCallback` 回调函数，以便在训练期间将模型上传到 Hub，并在拟合模型时添加该回调函数：

```python
from transformers.keras_callbacks import PushToHubCallback

callback = PushToHubCallback(output_dir="bert-finetuned-ner", tokenizer=tokenizer)

model.fit(
    tf_train_dataset,
    validation_data=tf_eval_dataset,
    callbacks=[callback],
    epochs=num_epochs,
)
```

你可以使用 `hub_model_id` 参数指定你想要推送的仓库的全名（特别需要注意的是，如果你需要推送给某个组织，就必须使用这个参数）。例如，当我们将模型推送到 [`huggingface-course` 组织](https://huggingface.co/huggingface-course) 时，我们添加了 `hub_model_id="huggingface-course/bert-finetuned-ner"` 。默认情况下，这个仓库将保存在你的账户之内，并以你设置的输出目录命名，例如 `"cool_huggingface_user/bert-finetuned-ner"` 。

<Tip>

💡 如果设置了模型保存的路径，并且在那里已经存在了一个非空的同名文件夹，那么该目录应该是 Hub 仓库克隆在本地的版本，如果不是，则会在调用 model.ﬁt() 方法时收到一个错误，并需要设置一个新的路径。

</Tip>

请注意，在训练过程中每次保存模型时（这里是每个 epooch），它都会在后台上传到 Hub。这样，如有需要，你将能够在另一台机器上继续你的训练。

到此阶段，你可以在模型 Hub 上使用推理小部件来测试你的模型并与你的朋友分享。你已经成功地在一个 tokens 分类任务上微调了一个模型——恭喜你！但我们的模型真的好用吗？我们应该找出一些指标来对模型进评估。

{/if}

### 评估指标 [[评估指标]]

{#if fw === 'pt'}

要让 `Trainer` 在每个周期计算一个指标，我们需要定义一个 `compute_metrics()` 函数，该函数的输入是预测值和标签的数组，并返回带有指标名称和评估结果的字典。

用于评估 Token 分类预测的经典框架是 [*seqeval*](https://github.com/chakki-works/seqeval) 。要使用此指标，我们首先需要安装 seqeval 库：

```py
!pip install seqeval
```

然后我们可以通过 `evaluate.load()` 函数加载它，就像我们在 [第三章](/course/chapter3) 中所做的那样：

{:else}

用于评估 Token 分类预测的经典框架是 [*seqeval*](https://github.com/chakki-works/seqeval) 。要使用此指标，我们首先需要安装 seqeval 库：

```py
!pip install seqeval
```

然后我们可以通过 `evaluate.load()` 函数加载它，就像我们在 [第三章](/course/chapter3) 中所做的那样：

{/if}

```py
import evaluate

metric = evaluate.load("seqeval")
```

该指标和常规的评测指标有些区别：它需要字符串形式的标签列表而不是整数，所以我们需要在将它们传递给指标之前将预测值和标签由数字解码为字符串。让我们先用一些测试数据看看它是如何工作的：

首先，获取第一个训练样本的标签。
```py
labels = raw_datasets["train"][0]["ner_tags"]
labels = [label_names[i] for i in labels]
labels
```

```python out
['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']
```

然后我们可以通过更改索引 2 处的值来为这些标签创建假的预测值来测试该指标：

```py
predictions = labels.copy()
predictions[2] = "O"
metric.compute(predictions=[predictions], references=[labels])
```

请注意，该指标的输入是预测列表（不是一个）和标签列表，输出结果如下：

```python out
{'MISC': {'precision': 1.0, 'recall': 0.5, 'f1': 0.67, 'number': 2},
 'ORG': {'precision': 1.0, 'recall': 1.0, 'f1': 1.0, 'number': 1},
 'overall_precision': 1.0,
 'overall_recall': 0.67,
 'overall_f1': 0.8,
 'overall_accuracy': 0.89}
```

{#if fw === 'pt'}

结果返回很多信息！包括每个单独实体及整体的准确率、召回率和 F1 分数。在这里我们将只保留总分，但是你可以自由地调整 `compute_metrics()` 函数返回的所需要指标。这个函数中我们首先会先取预测 `logits` 的 `argmax`，并将其转换为预测值。 `compute_metrics()` 函数首先取 `logits` 的 `argmax`，将它们转换为预测值（通常情况下，logits 和概率的顺序是相同，所以我们不需要使用 softmax）。然后我们需要将标签和预测值都从整数转换为字符串。我们删除所有标签为 `-100` 的值，最后将结果传递给 `metric.compute()` 方法：

```py
import numpy as np


def compute_metrics(eval_preds):
    logits, labels = eval_preds
    predictions = np.argmax(logits, axis=-1)

    # 删除忽略的索引(特殊 tokens )并转换为标签
    true_labels = [[label_names[l] for l in label if l != -100] for label in labels]
    true_predictions = [
        [label_names[p] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)
    ]
    all_metrics = metric.compute(predictions=true_predictions, references=true_labels)
    return {
        "precision": all_metrics["overall_precision"],
        "recall": all_metrics["overall_recall"],
        "f1": all_metrics["overall_f1"],
        "accuracy": all_metrics["overall_accuracy"],
    }
```

现在已经完成了，我们下面就可以开始定义我们的 `Trainer` 了。接下来我们只需要一个 `model` 并对其微调！

{:else}

它返回了大量的信息！我们获得每个单独实体以及整体的准确率、召回率和 F1 分数。现在，让我们看看如果我们尝试使用真实的模型预测来计算分数。

在使用基于TensorFlow 的框架时，我们通常不会把预测拼接在一起，因为这样会导致序列长度不统一。这意味着我们不能直接使用 `model.predict()` 方法 —— 但这并不能阻止我们。我们可以逐批获取一些预测并在在此的过程中将它们拼接成一个大的列表，删除表示 `masking/padding` 的 `-100` tokens 然后在合并后的大列表上计算度量值：

```py
import numpy as np

all_predictions = []
all_labels = []
for batch in tf_eval_dataset:
    logits = model.predict_on_batch(batch)["logits"]
    labels = batch["labels"]
    predictions = np.argmax(logits, axis=-1)
    for prediction, label in zip(predictions, labels):
        for predicted_idx, label_idx in zip(prediction, label):
            if label_idx == -100:
                continue
            all_predictions.append(label_names[predicted_idx])
            all_labels.append(label_names[label_idx])
metric.compute(predictions=[all_predictions], references=[all_labels])
```


```python out
{'LOC': {'precision': 0.91, 'recall': 0.92, 'f1': 0.91, 'number': 1668},
 'MISC': {'precision': 0.70, 'recall': 0.79, 'f1': 0.74, 'number': 702},
 'ORG': {'precision': 0.85, 'recall': 0.90, 'f1': 0.88, 'number': 1661},
 'PER': {'precision': 0.95, 'recall': 0.95, 'f1': 0.95, 'number': 1617},
 'overall_precision': 0.87,
 'overall_recall': 0.91,
 'overall_f1': 0.89,
 'overall_accuracy': 0.97}
```

与我们的模型相比，你的模型的表现如何？如果你看到类似的数字，那么你的训练就成功了！

{/if}

{#if fw === 'pt'}

### 定义模型

由于我们正在研究 Token 分类问题，因此我们将使用 `AutoModelForTokenClassification` 类。定义此模型时要记得传递我们标签的数量，最简单方法是将该数字传递给 `num_labels` 参数，但是如果我们想要一个就像我们在本节开头看到的那样的推理小部件，就需要用最标准的方法正确设置标签的对应关系。

最标准的方式是用两个字典 `id2label` 和 `label2id` 来设置标签，这两个字典包含从 ID 到标签的映射以及反向的映射：

```py
id2label = {str(i): label for i, label in enumerate(label_names)}
label2id = {v: k for k, v in id2label.items()}
```

现在我们只需将它们传递给 `AutoModelForTokenClassification.from_pretrained()` 方法，它们就会被保存在模型的配置中，然后被正确地保存和上传到 Hub：

```py
from transformers import AutoModelForTokenClassification

model = AutoModelForTokenClassification.from_pretrained(
    model_checkpoint,
    id2label=id2label,
    label2id=label2id,
)
```

就像我们在 [第三章](/course/chapter3) 中定义 `AutoModelForSequenceClassification` 类一样 创建模型会发出一个警告，提示一些权重未被使用（来自预训练头部的权重）和一些其他权重被随机初始化（来自新 Token 分类头部的权重），不过不用担心，我们马上就要训练这些权重。在这之前，让我们先确认一下我们的模型是否具有正确的标签数量：

```python
model.config.num_labels
```

```python out
9
```

<Tip warning={true}>

⚠️ 如果你的模型的标签数量有错误，那么在后面调用 `Trainer.train()` 时，你会得到一个晦涩的错误（类似于“CUDA error：device-side assert triggered”）。这可能会令人烦恼，所以确保你做了这个检查，确认你的标签数量是正确。

</Tip>

### 微调模型

我们现在准备好训练我们的模型了！在定义 `Trainer` 之前，我们只需要做最后两件事：登录 Hugging Face 并设置我们的训练参数。如果你在 notebook 上工作，有一个方便的小工具可以帮助你：

```python
from huggingface_hub import notebook_login

notebook_login()
```

登录后，我们就可以设置我们的 `TrainingArguments` ：

```python
from transformers import TrainingArguments

args = TrainingArguments(
    "bert-finetuned-ner",
    evaluation_strategy="epoch",
    save_strategy="epoch",
    learning_rate=2e-5,
    num_train_epochs=3,
    weight_decay=0.01,
    push_to_hub=True,
)
```

你已经对大多数内容有所了解了：我们设置了一些超参数（如学习率、训练的轮数和权重衰减），并设定 `push_to_hub=True` ，表示我们希望在每个训练轮次结束时保存并评估模型，然后将结果上传到模型中心。注意，你可以通过 `hub_model_id` 参数指定你想推送的仓库的名称（特别需要注意的是，如果你需要推送给某个组织，就必须使用这个参数）。例如，当我们将模型推送到 [`huggingface-course` 组织](https://huggingface.co/huggingface-course) 时，我们在 `TrainingArguments` 中添加了 `hub_model_id="huggingface-course/bert-finetuned-ner"` 。默认情况下，使用的仓库将保存在你的账户之内，并以你设置的输出目录命名，所以在我们的例子中，仓库的地址是 `"sgugger/bert-finetuned-ner"` 。

<Tip>

💡 如果你使用的输出路径已经存在一个同名的文件夹，那么它需要是你想推送到 hub 的仓库的克隆在本地的版本。如果不是，你将在声明 `Trainer` 时遇到一个错误，并需要设置一个新的路径。

</Tip>

最后，我们将所有内容传递给 `Trainer` 并启动训练：

```python
from transformers import Trainer

trainer = Trainer(
    model=model,
    args=args,
    train_dataset=tokenized_datasets["train"],
    eval_dataset=tokenized_datasets["validation"],
    data_collator=data_collator,
    compute_metrics=compute_metrics,
    tokenizer=tokenizer,
)
trainer.train()
```

请注意，在训练过程中每次保存模型时（这里是每个 epooch），它都会在后台上传到 Hub。这样，如有必要，你将能够在另一台机器上继续你的训练。

训练完成后，我们使用 `push_to_hub()` 上传模型的最新版本

```py
trainer.push_to_hub(commit_message="Training complete")
```

如果你想检查一下是否上传成功，这个命令会返回刚刚执行的提交的 URL：

```python out
'https://huggingface.co/sgugger/bert-finetuned-ner/commit/26ab21e5b1568f9afeccdaed2d8715f571d786ed'
```

同时 `Trainer` 还创建并上传了一张包含所有评估结果的模型卡。到此阶段，你可以在模型 Hub 上使用推理小部件来测试你的模型并与你的朋友分享。你已经成功地在一个 tokens 分类任务上微调了一个模型——恭喜你！

如果你想更深入地了解训练循环，我们现在将向你展示如何使用 🤗 Accelerate 做同样的事情。

## 自定义训练循环 [[自定义训练循环]]

现在我们来看看完整的训练循环，这样你就可以轻松地定制你需要的部分。它与我们在 [第三章](/course/chapter3/4) 中所做的内容很相似，但对评估部分有一些改动。

### 做好训练前的准备 [[做好训练前的准备]]

首先我们需要为我们的数据集构建 `DataLoader` 。我们将 `data_collator` 传递给 `collate_fn` 参数并随机打乱训练集，但不打乱验证集：

```py
from torch.utils.data import DataLoader

train_dataloader = DataLoader(
    tokenized_datasets["train"],
    shuffle=True,
    collate_fn=data_collator,
    batch_size=8,
)
eval_dataloader = DataLoader(
    tokenized_datasets["validation"], collate_fn=data_collator, batch_size=8
)
```

接下来我们重新实例化我们的模型，以确保我们不是继续之前的微调，而是重新开始从 BERT 微调预训练模型：

```py
model = AutoModelForTokenClassification.from_pretrained(
    model_checkpoint,
    id2label=id2label,
    label2id=label2id,
)
```

然后我们需要一个优化器。我们将使用经典 `AdamW` ，它类似于 `Adam` ，但在权重衰减的方式上进行了改进：

```py
from torch.optim import AdamW

optimizer = AdamW(model.parameters(), lr=2e-5)
```

当我们拥有了所有这些对象之后，我们就可以将它们发传递给 `accelerator.prepare()` 方法：

```py
from accelerate import Accelerator

accelerator = Accelerator()
model, optimizer, train_dataloader, eval_dataloader = accelerator.prepare(
    model, optimizer, train_dataloader, eval_dataloader
)
```

<Tip>

🚨 如果你正在 TPU 上训练，你需要将上面单元格开始的所有代码移动到一个专门的训练函数中。更多详情请回顾 [第三章](/course/chapter3) 。

</Tip>

现在我们已经将我们的 `train_dataloader` 传递给了 `accelerator.prepare()` 方法，我们还可以使用 `len()` 来计算训练步骤的数量。请记住，我们应该在准备好 `dataloader` 后再使用 `len()` ，因为改动 `dataloader` 会改变训练长度的数量。这里我们将使用一个从学习率衰减到 0 的经典线性学习率调度：

```py
from transformers import get_scheduler

num_train_epochs = 3
num_update_steps_per_epoch = len(train_dataloader)
num_training_steps = num_train_epochs * num_update_steps_per_epoch

lr_scheduler = get_scheduler(
    "linear",
    optimizer=optimizer,
    num_warmup_steps=0,
    num_training_steps=num_training_steps,
)
```

最后，为了将我们的模型推送到 Hub，我们需要在一个工作文件夹中创建一个 `Repository` 对象。如果你还没有登录的话，首先需要登录到 Hugging Face，然后根据模型 ID 来确定仓库名称（你可以将 `repo_name` 替换为你喜欢的名字；只需要包含你的用户名即可，你可以使用 `get_full_repo_name()` 函数的查看目前的 `repo_name` ）：

```py
from huggingface_hub import Repository, get_full_repo_name

model_name = "bert-finetuned-ner-accelerate"
repo_name = get_full_repo_name(model_name)
repo_name
```

```python out
'sgugger/bert-finetuned-ner-accelerate'
```

然后我们可以将该仓库克隆到本地文件夹中。如果本地已经存在同名的文件夹，这个本地文件夹必须是我们正在使用的仓库克隆在本地的版本：

```py
output_dir = "bert-finetuned-ner-accelerate"
repo = Repository(output_dir, clone_from=repo_name)
```

我们现在可以通过调用 `repo.push_to_hub()` 方法上传保存在 `output_dir` 中的所有内容。它帮助我们在每个训练周期结束时上传中间模型。

### 训练循环 [[训练循环]]

我们现在准备编写完整的训练循环。为了简化其评估部分，我们定义了一个 `postprocess()` 函数，该函数会接收模型的预测和真实的标签，并将它们转换为字符串列表，也就是我们的 `metric` （评估函数）对象需要的输入格式。

```py
def postprocess(predictions, labels):
    predictions = predictions.detach().cpu().clone().numpy()
    labels = labels.detach().cpu().clone().numpy()

    # 删除忽略的索引(特殊 tokens )并转换为标签
    true_labels = [[label_names[l] for l in label if l != -100] for label in labels]
    true_predictions = [
        [label_names[p] for (p, l) in zip(prediction, label) if l != -100]
        for prediction, label in zip(predictions, labels)
    ]
    return true_labels, true_predictions
```

然后我们可以编写训练循环。在定义一个进度条来跟踪训练的进度后，循环分为三个部分：

- 训练本身，这是经典的迭代过程，即在 `train_dataloader` 上进行迭代，在模型上前向传播训练数据，然后反向传递 `loss` 和优化参数
- 评估，在获取模型在一个 `batch` 上的输出之后，这里有一个需要注意的地方：由于两个进程可能已将输入和标签填充到不同的形状，我们需要使用 `accelerator.pad_across_processes()` 使预测和真实的标签在调用 `gather()` 方法之前具有相同的形状。如果我们不这样做，评估循环将会出错或无限期卡住。最后我们将结果发送到 `metric.add_batch()` 方法中，并在评估循环结束时调用 `metric.compute()` 方法。
- 保存和上传，首先保存模型和 `tokenizer` 然后调用 `repo.push_to_hub()` 方法。注意，我们使用参数 `blocking=False` 来告诉 🤗 `Hub` 库在一个异步进程中推送。这样，在训练时，这个指令在后台将模型和 `tokenizer` 推送到 `hub`。

以下是完整的训练循环代码：

```py
from tqdm.auto import tqdm
import torch

progress_bar = tqdm(range(num_training_steps))

for epoch in range(num_train_epochs):
    # 训练
    model.train()
    for batch in train_dataloader:
        outputs = model(**batch)
        loss = outputs.loss
        accelerator.backward(loss)

        optimizer.step()
        lr_scheduler.step()
        optimizer.zero_grad()
        progress_bar.update(1)

    # 评估
    model.eval()
    for batch in eval_dataloader:
        with torch.no_grad():
            outputs = model(**batch)

        predictions = outputs.logits.argmax(dim=-1)
        labels = batch["labels"]

        # 填充模型的预测和标签后才能调用 gathere()
        predictions = accelerator.pad_across_processes(predictions, dim=1, pad_index=-100)
        labels = accelerator.pad_across_processes(labels, dim=1, pad_index=-100)

        predictions_gathered = accelerator.gather(predictions)
        labels_gathered = accelerator.gather(labels)

        true_predictions, true_labels = postprocess(predictions_gathered, labels_gathered)
        metric.add_batch(predictions=true_predictions, references=true_labels)

    results = metric.compute()
    print(
        f"epoch {epoch}:",
        {
            key: results[f"overall_{key}"]
            for key in ["precision", "recall", "f1", "accuracy"]
        },
    )

    # 保存并上传
    accelerator.wait_for_everyone()
    unwrapped_model = accelerator.unwrap_model(model)
    unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
    if accelerator.is_main_process:
        tokenizer.save_pretrained(output_dir)
        repo.push_to_hub(
            commit_message=f"Training in progress epoch {epoch}", blocking=False
        )
```

如果这是你第一次看到使用 🤗 Accelerate 保存模型，让我们花点时间来了解一下这个过程中的三行代码：

```py
accelerator.wait_for_everyone()
unwrapped_model = accelerator.unwrap_model(model)
unwrapped_model.save_pretrained(output_dir, save_function=accelerator.save)
```

第一行是不言自明的：它告诉所有的进程先等待，直到所有的进程都处于这个阶段再继续（阻塞）。这是为了确保在保存之前，我们在每个进程中都有相同的模型。
第二行代码用于获取 `unwrapped_model` ，它就是我们定义的基本模型。 `accelerator.prepare()` 方法会为了在分布式训练中工作而对模型进行了一些修改，所以它不再有 `save_pretraining()` 方法；使用 `accelerator.unwrap_model()` 方法可以撤销对模型的更改。
在第三行代码中，我们调用 `save_pretraining()` ，并指定 `accelerator.save()` 作为 `save_function` 而不是默认的 `torch.save()` 。

完成这些操作后，你应该拥有一个与 `Trainer` 训练出的模型结果相当类似的模型。你可以在 [huggingface-course/bert-finetuned-ner-accelerate](https://huggingface.co/huggingface-course/bert-finetuned-ner-accelerate) 查看我们使用这些代码训练的模型。如果你想在训练循环中测试任何调整，你可以直接通过编辑上面显示的代码来实现它们！

{/if}

## 使用微调模型 [[使用微调模型]]

我们已经向你展示了如何使用在模型中心微调的模型和推理小部件。在本地使用 `pipeline` 来使用它非常容易，你只需要指定正确的模型标签：

```py
from transformers import pipeline

# 将此替换为你自己的 checkpoint
model_checkpoint = "huggingface-course/bert-finetuned-ner"
token_classifier = pipeline(
    "token-classification", model=model_checkpoint, aggregation_strategy="simple"
)
token_classifier("My name is Sylvain and I work at Hugging Face in Brooklyn.")
```

```python out
[{'entity_group': 'PER', 'score': 0.9988506, 'word': 'Sylvain', 'start': 11, 'end': 18},
 {'entity_group': 'ORG', 'score': 0.9647625, 'word': 'Hugging Face', 'start': 33, 'end': 45},
 {'entity_group': 'LOC', 'score': 0.9986118, 'word': 'Brooklyn', 'start': 49, 'end': 57}]
```

太棒了！我们的模型与此管道的默认模型一样有效！
